﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace Vcm
{
    public class CryptCardMaker : CardMakerBase
    {
        //private static PdfContentByte _cb;

        public override float MaxTextFontSize
        {
            get { return 9.2f; }
        }

        public override bool SupportsRow(DataRow cardRow)
        {
            try
            {
                var card = new CryptCardData(cardRow);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        public override void AddCard(ProxyDocument proxyDocument, DataRow cardRow, out string outputMessage)
        {
            var card = new CryptCardData(cardRow);

            var doc = proxyDocument.Document;
            var writer = proxyDocument.Writer;

            var outputMessageBuilder = new StringBuilder();

            Debug.WriteLine("Processing card " + card.Name);
            outputMessageBuilder.AppendLine("Processing card " + card.Name);

            while (--card.Copies >= 0)
            {
                float x, y;
                GetNewCardsCoordinates(proxyDocument, doc, out x, out y);

                var cb = writer.DirectContent;
                //_cb = cb;

                EncounteredClanNames.Clear();
                CardName = card.CorrectedName;

                var xBorderImage = x - (CardWidthBorder - CardWidth) / 2f;
                var yBorderImage = y - (CardHeightBorder - CardHeight) / 2f;
                var xArtImage = x + CardWidth * 0.1855f;
                var yArtImage = y + CardHeight * 0.207f;
                const float wArtImage = CardWidth * 0.777f;
                const float hArtImage = CardHeight * 0.708f;

                if (proxyDocument.UseGhost)
                {
                    cb.SetLineWidth(1f);
                    cb.SetRGBColorStroke(0, 0, 0);
                    cb.Rectangle(xBorderImage, yBorderImage, CardWidthBorder, CardHeightBorder);
                    cb.Stroke();
                    cb.Ellipse(xArtImage, yArtImage, xArtImage + wArtImage, yArtImage + hArtImage);
                    cb.Stroke();
                    cb.Rectangle(x, y, CardWidth, CardHeight);
                    cb.Stroke();
                }
                else
                {
                    // draw the black border by stretching a black box to the desired size
                    var borderImage = ImageProvider.GetAbsoluteSizedImage(">blackbox", string.Format(@"{0}Images\{1}", ResourcesPath, "blackbox"), ImageExtensions, CardWidthBorder, CardHeightBorder);
                    borderImage.SetAbsolutePosition(xBorderImage, yBorderImage);
                    doc.Add(borderImage);

                    // draw the card art
                    var artImagePath = String.Format("{0}\\{1}", proxyDocument.ArtPath, NormalizeImageName(card.Name));
                    if (card.IsAdv)
                        artImagePath += " (Adv)";
                    var artImage = ImageProvider.GetAbsoluteSizedImage("", artImagePath, ImageExtensions, wArtImage, hArtImage);
                    if (artImage != null)
                    {
                        artImage.SetAbsolutePosition(xArtImage, yArtImage);
                        doc.Add(artImage);
                    }
                    else
                    {
                        outputMessageBuilder.AppendLine("<!> Couldn't find artwork file " + artImagePath + " with any of these extensions: " + string.Join(", ", ImageExtensions));
                        // to save ink, draw a white box
                        var noArtImage = ImageProvider.GetAbsoluteSizedImage(">whitebox", string.Format(@"{0}Images\{1}", ResourcesPath, "whitebox"), ImageExtensions, wArtImage, hArtImage);
                        noArtImage.SetAbsolutePosition(xArtImage, yArtImage);
                        doc.Add(noArtImage);
                    }

                    // draw the card background
                    var backgroundName = string.Equals(card.Type, "Imbued", StringComparison.InvariantCultureIgnoreCase)
                        ? "_imbued" + card.Clan
                        : "_" + card.Clan;

                    var backgroundImage = ImageProvider.GetAbsoluteSizedImage(">clanbackground-" + backgroundName, string.Format(@"{0}Images\{1}", ResourcesPath, backgroundName), ImageExtensions, CardWidth, CardHeight);
                    //var backgroundImage = ImageProvider.GetAbsoluteSizedImage("alu" + backgroundName, string.Format(@"{0}Images\{1}", ResourcesPath, "alu"), ImageExtensions, CardWidth, CardHeight);
                    if (backgroundImage != null)
                    {
                        backgroundImage.SetAbsolutePosition(x, y);
                        doc.Add(backgroundImage);
                    }
                    else
                    {
                        outputMessageBuilder.AppendLine("<!> Couldn't find background file " + backgroundName + " with any of these extensions: "
                            + string.Join(", ", ImageExtensions));
                        outputMessageBuilder.AppendLine("Please check that the clan name is correct in the CSV file (eg. Ahrimane and not Ahrimanes)");
                    }
                }

                // prepare the card name position
                var xName = x + CardWidth * 0.022f;
                var yName = y + CardHeight * 0.932f;
                var wName = CardWidth * 0.9f;

                // write the expansion symbol
                if (proxyDocument.ExpansionSymbolPath != null)
                {
                    const float maxWidth = CardWidth * 0.09f;
                    const float maxHeight = CardHeight * 0.14f;
                    var xExpansionSymbol = x + CardWidth * .865f;
                    var yExpansionSymbol = y + CardHeight * .81f;

                    // debug expansion symbol outline
                    //cb.SetRGBColorStroke(255, 0, 0);
                    //cb.Rectangle(xExpansionSymbol, yExpansionSymbol, maxWidth, maxHeight);
                    //cb.Stroke();

                    var expansionSymbolImage = ImageProvider.GetFittingImage(">cryptExpansionSymbol", proxyDocument.ExpansionSymbolPath, maxWidth, maxHeight);
                    xExpansionSymbol += (maxWidth - expansionSymbolImage.ScaledWidth) / 2f;
                    yExpansionSymbol += (maxHeight - expansionSymbolImage.ScaledHeight) / 2f;

                    expansionSymbolImage.SetAbsolutePosition(xExpansionSymbol, yExpansionSymbol);
                    doc.Add(expansionSymbolImage);

                    if (yExpansionSymbol + expansionSymbolImage.ScaledHeight > yName) // icon overlaps on card name area
                        wName = xExpansionSymbol - xName;
                }

                // write the card name
                var nameFont = BaseFont.CreateFont(ResourcesPath + @"Fonts\matrixb.ttf", BaseFont.CP1252, BaseFont.EMBEDDED);
                var fonts = new BaseFont[7];
                fonts[(int)FontType.Regular] = nameFont;

                WriteName(doc, cb, card.CorrectedName, xName, yName, wName, 12, fonts, proxyDocument.UseGhost ? 0 : 0xFFFFFF);

                // write the card disciplines
                var xDiscipline = x + CardWidth * 0.0245f;
                var yDiscipline = y + CardHeight * 0.090f;

                var disciplines = card.Disciplines;

                var disciplinesText = String.Empty;
                foreach (var discipline in disciplines)
                {
                    var disciplineKey = "[" + discipline + "]";
                    DisciplineInfo disciplineInfo;
                    if (Disciplines.TryGetValue(disciplineKey, out disciplineInfo))
                    {
                        if (discipline.ToLower() == discipline)
                            disciplinesText += InferiorDiscipline + disciplineInfo.Text;
                        else
                            disciplinesText += SuperiorDiscipline + disciplineInfo.Text;
                    }
                    else
                    {
                        disciplineKey = "[" + discipline.ToLower() + "]";
                        if (Disciplines.TryGetValue(disciplineKey, out disciplineInfo))
                        {
                            disciplinesText += SuperiorDiscipline + disciplineInfo.Text.ToUpper();
                        }
                        else
                            outputMessageBuilder.AppendLine("<!> Couldn't find discipline symbol for " + discipline);
                    }
                }

                // write the disciplines from top to bottom, starting with the last discipline
                const int disciplineSize = 16;
                var disciplinesCount = disciplinesText.Length / 2;
                for (var i = disciplinesCount - 1; i >= 0; --i)
                {
                    if (disciplinesText[i * 2] == '>') // superior level
                        yDiscipline += disciplineSize * 0.25f;
                    WriteSymbolCentered(cb, xDiscipline, yDiscipline, SymbolFont, disciplineSize, disciplinesText[i * 2], disciplinesText[i * 2 + 1]);
                    yDiscipline += disciplineSize;
                }

                // write the advanced symbol
                if (card.IsAdv)
                {
                    var xAdvancedSymbol = x + CardWidth * 0.055f;
                    var yAdvancedSymbol = y + CardHeight * 0.707f;
                    WriteSymbolCentered(cb, xAdvancedSymbol, yAdvancedSymbol, SymbolFont, 14, '_', '|');
                }

                // write the card text
                const float wTextBox = CardWidth * 0.765f;
                const float hTextBox = CardHeight * 0.141f;

                var xTextBox = x + CardWidth * 0.1925f;
                var yTextBox = y + CardHeight * 0.049f;

                fonts[(int)FontType.Regular] = TextFont;
                fonts[(int)FontType.Bold] = BoldTextFont;
                fonts[(int)FontType.Italic] = ItalicTextFont;
                fonts[(int)FontType.BoldItalic] = BoldItalicTextFont;
                fonts[(int)FontType.Symbol] = SymbolFont;
                fonts[(int)FontType.Condensed] = CondensedFont;

                var xCenterCapacity = x + CardWidth * 0.935f;
                var yCenterCapacity = y + CardHeight * 0.058f;
                const float radiusCapacity = CardWidth * 0.05f;

                // debug text box outline
                //cb.SetRGBColorStroke(255, 0, 0);
                //cb.Rectangle(xTextBox, yTextBox, wTextBox, hTextBox);
                //cb.Stroke();

                if (proxyDocument.UseGhost)
                {
                    cb.SetLineWidth(1f);
                    cb.SetRGBColorStroke(0, 0, 0);
                    //cb.Rectangle(capacityXCenter - 1, capacityYCenter - 1, 2, 2);
                    cb.Circle(xCenterCapacity, yCenterCapacity, radiusCapacity);
                    cb.Stroke();
                }
                // add some exclusion functions so that the box of a word doesn't hit the capacity circle.
                // since we don't use centered text in crypt cards, we'll never check the symetrical exclusion box
                // var symetricalCapacityXCenter = xTextBox - (capacityXCenter - (xTextBox + textWidth));
                var exclusionAreaFuncs = new List<Func<Rectangle, bool, bool>>
                {
                    (r, isSymetrical) =>
                        {
                            bool result = Intersects(xCenterCapacity, yCenterCapacity, radiusCapacity, r);
                            //if (isSymetrical && !result)
                            //    result = Intersects(symetricalCapacityXCenter, capacityYCenter, capacityRadius, r);
                            return result;
                        }
                };

                // write the watermark
                if (!string.IsNullOrEmpty(proxyDocument.Watermark))
                {
                    bool overArt = proxyDocument.WatermarkPosition == CardPosition.ArtBox;
                    WriteWatermark(cb, proxyDocument.Watermark, overArt ? xArtImage : xTextBox, overArt ? yArtImage : yTextBox,
                        overArt ? wArtImage : wTextBox, overArt ? hArtImage : hTextBox, nameFont, proxyDocument.UseGhost, proxyDocument.WatermarkOrientation, proxyDocument.WatermarkColor, proxyDocument.WatermarkOpacity);
                }

                //test line to check that the text avoids the capacity circle
                //var text = new StringBuilder();
                //for (int i = 0; i < 3000; ++i)
                //    text.Append("a ");
                //card.Text = text.ToString();

                const float hMention = 5f;
                float spaceForMention = proxyDocument.NotForLegalPlay ? hMention : 0f;

                WriteTextBlock(doc, cb, xTextBox, yTextBox + spaceForMention, wTextBox, hTextBox - spaceForMention, fonts, card.Text, "Vampire", exclusionAreaFuncs);

                if (proxyDocument.NotForLegalPlay)
                {
                    WriteTextCentered(cb, "NOT FOR LEGAL PLAY", xTextBox + wTextBox / 2f, yTextBox, fonts[(int)FontType.Bold], hMention, 0);
                }

                // write capacity
                var cardinalFont = BaseFont.CreateFont(ResourcesPath + @"Fonts\quorum_black_bt.ttf", BaseFont.CP1252, BaseFont.EMBEDDED);
                WriteTextCentered(cb, card.Capacity, xCenterCapacity, yCenterCapacity - CardHeight * 0.017f, cardinalFont, 12f, proxyDocument.UseGhost ? 0 : 0xFFFFFF);

                // write group
                var xGroup = x + CardWidth * 0.175f;
                var yGroup = y + CardHeight * 0.195f;
                WriteText(cb, card.Group, xGroup, yGroup, BoldTextFont, 7.5f, proxyDocument.UseGhost ? 0 : 0xFFFFFF, 0x000000, 100, 0.1f);

                //var blackForegroundClans = new[] { "True Brujah", "Ahrimane" };

                // write the illustrator name
                var xIllustrator = x + CardWidth * 0.16f;
                var yIllustrator = y + CardHeight * 0.01f;
                //if (blackForegroundClans.Contains(card.Clan))
                //    WriteText(cb, xIllustrator, yIllustrator, TextFont, 6f, "Illus: " + card.Illustrator, 0xFFFFFF, scaling: 75f, outlineColor: 0, lineWidth: 0.01f);
                //else
                WriteText(cb, "Illus: " + card.Illustrator, xIllustrator, yIllustrator, TextFont, 6f, proxyDocument.UseGhost ? 0 : 0xFFFFFF, scaling: 75f);

                // write the copyright
                var xCopyright = x + CardWidth * 0.97f;
                var yCopyright = y + CardHeight * 0.007f;

                //if (blackForegroundClans.Contains(card.Clan))
                //    WriteRightAlignedText(cb, xCopyright, yCopyright, TextFont, 4f, proxyDocument.Copyright, 0xFFFFFF, scaling: 75f, outlineColor: 0, lineWidth: 0.01f);
                //else
                WriteRightAlignedText(cb, proxyDocument.Copyright, xCopyright, yCopyright, TextFont, 4f, proxyDocument.UseGhost ? 0 : 0xFFFFFF, scaling: 75f);

                #region Temporary
                // write clan (temporary until background is fixed)
                // write the card clan (currently using bitmap)
                var clanSymbolInfo = ClanWidthFactors.ValueOrDefault(card.Clan, DefaultClanSymbolInfo);

                if (proxyDocument.UseGhost || clanSymbolInfo.ShowOnCryptCard)
                {
                    var xClanSymbol = x + CardWidth * 0.075f;
                    var yClanSymbol = y + CardHeight * 0.847f;

                    var wClanSymbol = CardWidth * clanSymbolInfo.WidthFactor * .98f;
                    if (clanSymbolInfo.IsRotated)
                        wClanSymbol *= 2f;
                    Image cardClanSymbol = null;
                    if (proxyDocument.UseGhost || clanSymbolInfo.IsRotated)
                        cardClanSymbol = ImageProvider.GetFittingImage("clanIcon-" + card.Clan, string.Format(@"{0}Images\ClanIcon-{1}", ResourcesPath, card.Clan), ImageExtensions, wClanSymbol, CardHeight);
                    if (cardClanSymbol == null)
                        cardClanSymbol = ImageProvider.GetFittingImage("clanSymbol-" + card.Clan, string.Format(@"{0}Images\ClanSymbol-{1}", ResourcesPath, card.Clan), ImageExtensions, wClanSymbol, CardHeight);
                    if (cardClanSymbol != null)
                    {
                        var realXClanSymbol = clanSymbolInfo.IsRotated ? xClanSymbol - CardWidth * 0.05f : xClanSymbol - wClanSymbol / 2f;

                        // compute the clan symbol vertical position
                        var hClanSymbol = cardClanSymbol.ScaledHeight;
                        var realYClanSymbol = yClanSymbol - (clanSymbolInfo.IsRotated ? hClanSymbol - CardHeight * 0.03f : hClanSymbol / 2f);

                        // draw the clan symbol
                        cardClanSymbol.SetAbsolutePosition(realXClanSymbol, realYClanSymbol);

                        //cb.SetRGBColorStroke(255, 0, 0);
                        //cb.Rectangle(realXClanSymbol, realYClanSymbol, cardClanSymbol.ScaledWidth, cardClanSymbol.ScaledHeight);
                        //cb.Stroke();

                        doc.Add(cardClanSymbol);
                    }
                }
                #endregion

                // debug card outlines
                //cb.SetRGBColorStroke(255, 0, 0);
                //cb.Rectangle(x - (CardWidthBorder - CardWidth) / 2f, y - (CardHeightBorder - CardHeight) / 2f, CardWidthBorder, CardHeightBorder);
                //cb.Stroke();
                //cb.SetRGBColorStroke(255, 0, 0);
                //cb.Rectangle(x , y , CardWidth, CardHeight);
                //cb.Stroke();

                ++proxyDocument.ImageIndex;
                if (proxyDocument.ImageIndex % 9 == 0)
                    DrawCutLines(cb);
            }

            outputMessage = outputMessageBuilder.ToString();
        }

        /// <summary>
        /// Write text trying to maximize the font size.
        /// </summary>
        protected void WriteTextBlock(Document doc, PdfContentByte cb, float x, float y, float width, float height, BaseFont[] fonts, string text, string cardType, ICollection<Func<Rectangle, bool, bool>> exclusionAreaFuncs)
        {
            //cb.Rectangle(x, y, width, height);
            //cb.Stroke();

            //get the segments
            var segmentsLines = new List<IList<Segment>>();
            var lines = text.Split('\n');

            //if (lines.Length > 1 && !string.IsNullOrWhiteSpace(lines[1]))
            //    throw new Exception("Crypt multiline is not supported");

            var mergeTextIndex = text.IndexOf("[MERGED]", StringComparison.InvariantCulture);
            if (mergeTextIndex > 0)
            {
                text = text.Insert(mergeTextIndex, "\n");
                lines = text.Split('\n').Select(l => l.Trim()).ToArray();
            }

            lines = lines.Select(l => l.Trim()).Where(l => l.Length > 0).ToArray();

            // apply the default styles
            if (cardType == "Vampire") // bold before :
            {
                bool isHighlighted = false;
                for (int i = 0; i < lines.Length; i++)
                {
                    var line = lines[i];
                    var segments = GetSegments(line, ref isHighlighted);
                    if (i == 0 || segments.Any(segment => segment.Text.EndsWith(":")))
                    {
                        foreach (var segment in segments)
                        {
                            segment.IsBold = true;
                            if (segment.Text.EndsWith(":"))
                            {
                                segment.NoUnbreakableSpaceAfterColumn = true;
                                break;
                            }
                        }
                    }
                    else if (i == 1) // second line, Text is bold if it contains no verb (eg. Dominique "Independent Anarch Baron of Paris.") and doesn't start with "+..." (eg. +1 strength)
                    {
                        if (segments[1].Text[0] != '+' && segments[1].Text[0] != '-')
                        {
                            if (segments.All(s => !IsVerb(s.Text)) || segments.Any(s => s.Text.EndsWith(":")))
                            {
                                foreach (var segment in segments)
                                {
                                    segment.IsBold = true;
                                    if (segment.Text.EndsWith(":"))
                                    {
                                        segment.NoUnbreakableSpaceAfterColumn = true;
                                        break;
                                    }
                                }
                            }
                        }
                    }
                    segmentsLines.Add(segments);
                }
            }
            //else if (cardType != "Imbued")
            //{
            //    var isBold = (firstLine && (lines.Length > 1 || cardType == "Ally") && !segments.First().IsSymbol) // keywords or condition
            //                 || (!firstLine && segments.First().IsSymbol && segments.First().Text[0] == '>'); // superior discipline effect
            //    if (isBold)
            //    {
            //        foreach (var segment in segments)
            //            segment.IsBold = true;
            //    }
            //}

            // group the segments
            var segmentsGroupsLines = segmentsLines.Select(GroupSegments).ToList();

            // try to make the text fit
            var fontSize = Math.Min(MaxTextFontSize, (float)Math.Round(height / lines.Length));
            bool isFitting = false;
            float remainingVerticalSpace;

            var segmentWidths = new List<float>();
            while (!isFitting && fontSize > 0)
            {
                // first, we try to make it fit when aligned to top
                // then we'll try to move the text towards the center of the box to make it vertically centered.
                var yTop = y + height - fontSize;

                segmentWidths.Clear();
                isFitting = CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, x, yTop, width, height, fonts, fontSize, 0x000000, out remainingVerticalSpace, exclusionAreaFuncs, segmentWidths);
                if (!isFitting)
                {
                    fontSize -= 0.1f;
                    continue;
                }

                // check the line lengths: the first line mustn't be much shorter than the second one (by 20%)
                if (segmentWidths.Count > 1 && segmentWidths[0] < segmentWidths[1] && (segmentWidths[1] - segmentWidths[0]) / segmentWidths[0] > 0.2f)
                {
                    isFitting = false;
                    fontSize -= 0.1f;
                    continue;
                }

                // try to reduce the number of lines by slightly reducing the font size
                var fontSizeTest = fontSize;
                var minLineCount = segmentWidths.Count;
                //var fontSizeChanged = false;
                var minimumFontSize = fontSize - 0.5;
                while (fontSizeTest >= minimumFontSize)
                {
                    segmentWidths.Clear();
                    CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, x, yTop, width, height, fonts, fontSizeTest, 0x000000, out remainingVerticalSpace, exclusionAreaFuncs, segmentWidths);
                    if (segmentWidths.Count < minLineCount)
                    {
                        //fontSizeChanged = true;
                        fontSize = fontSizeTest;
                        minLineCount = segmentWidths.Count;
                    }
                    fontSizeTest -= 0.1f;
                }

                // it fits, try to move it towards the center vertically
                var lastRemainingVerticalSpace = remainingVerticalSpace;
                var yTest = yTop - lastRemainingVerticalSpace / 2;

                var lineCount = minLineCount;

                while (yTest <= yTop)
                {
                    segmentWidths.Clear();
                    isFitting = CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, x, yTest, width, height, fonts, fontSize, 0x000000, out remainingVerticalSpace, exclusionAreaFuncs, segmentWidths);

                    // if the vertical centering introduced another line (due to the capacity circle), then we're to low to display
                    if (segmentWidths.Count <= lineCount // I can't imagine why centered the number of line could be reduced, but if it does, it's perfect too :) Otherwise test for ==
                        && isFitting)
                        break;
                    yTest += 0.1f;
                }
                if (yTest > yTop)
                {
                    y = yTop; // the starting position worked, we don't want to go above it
                    isFitting = true;
                }
                else if (yTest - (yTop - lastRemainingVerticalSpace / 2) >= fontSize * 0.4f) // we moved too much from the center position
                {
                    // we'll try again by slighty reducing the font size.
                    isFitting = false;
                    fontSize -= 0.1f;
                }
                else
                    y = yTest; // the y position was found
            }
            if (!isFitting)
                throw new Exception("Can't make all the text fit: " + text);

            // perform the rendering
            CheckIfTextBlockFitsOrWrite(doc, cb, segmentsGroupsLines, x, y, width, height, fonts, fontSize, 0x000000, out remainingVerticalSpace, exclusionAreaFuncs, null);
        }

        private bool IsVerb(string text)
        {
            var verbs = new[] { "is", "gets", "may", "has", "untaps", "can", "cost" };
            return verbs.Contains(text);
        }

        protected static bool CheckIfTextBlockFitsOrWrite(Document doc, PdfContentByte cb, IList<IList<SegmentGroup>> segmentsGroupsLines,
            float x, float y, float width, float height,
            BaseFont[] fonts, float fontSize, int foreColor, out float remainingVerticalSpace, ICollection<Func<Rectangle, bool, bool>> exclusionAreaFuncs, List<float> segmentWidths)
        {
            var currentHeight = 0f;
            int lineCount = 0;
            if (segmentsGroupsLines.Any(segmentsGroups =>
                !AppendOrWriteSegments(doc, cb, segmentsGroups, x, y, width, ref currentHeight, fonts, fontSize, false, foreColor, null, segmentWidths, ref lineCount, false, exclusionAreaFuncs: exclusionAreaFuncs)
                || currentHeight > height))
            {
                remainingVerticalSpace = 0f;
                return false;
            }

            remainingVerticalSpace = height - currentHeight;
            return true;
        }

        public override void FixCard(StreamWriter outputFile, DataRow cardRow, out string outputMessage)
        {
            var result = new List<string>();
            var card = new CryptCardData(cardRow);
            outputMessage = "";

            var newData = new Dictionary<string, string>();

            if (cardRow["Type"].ToString() == "Vampire")
            {
                var disciplines = new List<string>();
                var referenceDisciplines = Disciplines.Where(d => d.Key.Length > 0 && char.IsLower(d.Key[1])).OrderBy(d => d.Value.Name).ToList();
                foreach (var discipline in referenceDisciplines)
                {
                    if (cardRow[discipline.Value.Name].ToString() == "1")
                        disciplines.Add(discipline.Key.Substring(1, 3));
                }
                foreach (var discipline in referenceDisciplines)
                {
                    if (cardRow[discipline.Value.Name].ToString() == "2")
                        disciplines.Add(discipline.Key.Substring(1, 3).ToUpper());
                }
                newData["Disciplines"] = disciplines.Count == 0
                    ? "-none-"
                    : string.Join(" ", disciplines);
            }

            var cardText = cardRow["Card Text"].ToString();
            var openCount = cardText.Count(c => c == '{');
            var closeCount = cardText.Count(c => c == '}');

            if (openCount != closeCount)
                outputMessage += "Mismatch count for { and }.";

            cardText = cardText.Replace("{", null).Replace("}", null);
            if (cardText.Length == 0)
                outputMessage += "Empty card text.";
            else if (cardText[cardText.Length - 1] != '.')
                outputMessage += "Card text doesn't end with a mark.";
            if (cardText.Contains("  "))
                outputMessage += "Card text contains multiple contiguous spaces.";

            foreach (var column in cardRow.Table.Columns)
            {
                var columnName = column.ToString();
                var value = cardRow[columnName].ToString();

                if (columnName != "Card Text" && (value.Contains("{") || value.Contains("}")))
                    outputMessage += "Column " + columnName + " contains errata.";
            }

            foreach (var column in cardRow.Table.Columns)
            {
                var columnName = column.ToString();
                var value = cardRow[columnName].ToString();

                string newValue;
                if (newData.TryGetValue(columnName, out newValue))
                    value = newValue;
                result.Add(value);
            }

            var escapedAndQuotedLine = string.Join(",", result.Select(EscapeAndQuote));
            outputFile.WriteLine(escapedAndQuotedLine);
            if (outputMessage.Length > 0)
                outputMessage = cardRow["Name"] + Environment.NewLine + outputMessage;
        }
    }
}
